#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Author  : Leo.W
@Date    : 2024/2/22 14:08
@File    : 11-class.py
@Software: PyCharm

[面向对象]
"""
from time import time, localtime, sleep

"""
=== 作用域和命名空间 ===
命名空间(Namespace)：是一个用于存储变量名称和其对应对象的系统，用于区分不同变量的作用范围和可见性。它可以被看作是一个字典，其中变量名是字典中的键，对应的对象是字典中的值。

Python中的命名空间可以分为以下几种类型：
 1.内置命名空间(Built-in Namespace)
    这是Python解释器内置的命名空间，它包含了Python的内置函数和异常等。这些名称可以在任何地方直接使用，无需导入任何模块。

 2.全局命名空间(Global Namespace)
    全局命名空间是指在模块层级定义的命名空间，它在整个模块中可见。模块中的全局变量和函数都属于全局命名空间。
    全局命名空间也可以被其他模块访问，通过导入模块后使用模块名.变量名或模块名.函数名的方式进行访问。

 3.局部命名空间(Local Namespace)
    局部命名空间是指在函数、方法或类中定义的命名空间。它仅在所定义的函数、方法或类的作用范围内可见。
    每当函数、方法或类被调用时，都会创建一个新的局部命名空间，用于存储局部变量和参数。
    当函数、方法或类执行完毕后，局部命名空间将被销毁。

 4.闭包命名空间(Closure Namespace)
    闭包命名空间是在【嵌套函数】中创建的命名空间。
    当一个内部函数引用了外部函数的变量时，内部函数形成了一个闭包，并创建了一个闭包命名空间。
    闭包命名空间在【内部函数被调用时】被创建，保存了【外部函数的变量】引用，以【供内部函数】在【外部函数执行完毕后】继续访问。

 此外，还有一些其他类型的命名空间，如类命名空间(Class Namespace)和实例命名空间(Instance Namespace)，用于存储类和实例相关的属性和方法。

在Python中，命名空间的查找顺序遵循了一个称为 LEGB 规则的顺序：
 [1.局部命名空间(Local)]
    首先搜索当前局部命名空间，即函数、方法或类中的命名空间。
 [2.闭包命名空间(Enclosing)]
    如果在当前局部命名空间中找不到变量，Python会继续搜索闭包命名空间，即包含当前局部命名空间的外部函数的命名空间。
 [3.全局命名空间(Global)]
    如果在闭包命名空间中找不到变量，Python会搜索全局命名空间，即模块级别的命名空间。
 [4.内置命名空间(Built-in)]
    如果在全局命名空间中找不到变量，最后会搜索内置命名空间，即Python解释器的内置函数和异常。

作用域虽然是被静态确定的，但会被动态使用。执行期间的任何时刻，都会有 3 或 4 个“命名空间可直接访问”的嵌套作用域：
 1.最内层作用域，包含局部名称，并首先在其中进行搜索
 2.那些外层闭包函数的作用域，包含“非局部、非全局”的名称，从最靠内层的那个作用域开始，逐层向外搜索。
 3.倒数第二层作用域，包含当前模块的全局名称
 4.最外层（最后搜索）的作用域，是内置名称的命名空间

global 语句：用于表明特定变量在全局作用域里，并应在全局作用域中重新绑定。
nonlocal 语句：表明特定变量在外层作用域中，并应在外层作用域中重新绑定。

"""
count = 1  # 定义全局变量


def scope_test():
    count = 2

    def do_local():
        spam = "local spam"  # 闭包中定义的局部变量，作用域闭包，不会改变scope_test函数定义的局部变量spam。

    def do_nonlocal():
        # nonlocal可以让最里面的函数使用最近的一个外函数已声明的变量，将最里面的函数的局部变量设置和最近的一个外函数声明的变量为同一个变量（引用同一个内存地址）
        # nonlocal 声明的变量只对 局部命名空间 起作用，离开封装函数，那么该变量就无效
        # nonlocal 赋值会改变外部函数 scope_test 中 spam 的值
        nonlocal spam  # nonlocal spam 指的是函数 do_nonlocal 内
        spam = "nonlocal spam"
        # 注意：nonlocal 不会使用全局变量，仅指向上一层的同名变量
        nonlocal count  # 如果上层函数为定义 count 将报错： SyntaxError: no binding for nonlocal 'count' found
        print(count)  # 2
        count += 1

    def do_global():
        # global 声明 spam 为全局变量
        # 注意：虽然是在scope_test内部，但不会改变 scope_test.spam 的局部变量，说明global全局声明对局部的变量不产生作用。
        global spam
        spam = "global spam"

    spam = "test spam"
    do_local()  # do_local函数内部的变量不会影响到函数外部的变量
    print("After local assignment:", spam)  # test spam
    do_nonlocal()  # do_nonlocal 中 nonlocal 声明的变量和 do_nonlocal外的同名变量为同一个变量（引用同一个内存地址）
    print("After nonlocal assignment: spam ->", spam)  # spam -> nonlocal spam
    print("After nonlocal assignment: count ->", count)  # count -> 3
    do_global()
    print("After global assignment:", spam)  # nonlocal spam


scope_test()
print("In global scope:", spam)  # global spam

# After local assignment: test spam
# After nonlocal assignment: spam -> nonlocal spam
# After nonlocal assignment: count -> 3
# After global assignment: nonlocal spam
# In global scope: global spam

"""
使用class关键字定义类
类名是用大写字母开头的驼峰法命名

=== 属性和方法的访问权限 ===
只有两种：公开的 和 私有的：
    1. 私有属性/方法，在给属性命名时可以用【两个下划线】 __ 作为开头，子类无法访问
    2. 另一种命名惯例就是让属性名以【单下划线】_ 开头,来表示属性是受保护的，本类之外的代码在访问这样的属性时应该要保持慎重。
    这种做法并不是语法上的规则，单下划线开头的属性和方法外界仍然是可以访问的，所以更多的时候它是一种暗示或隐喻
"""


class Person:  # 理论上所有的类都继承自 object,相当于 Person(object)
    """Person类"""

    kind = "human"  # 声明在 __init__()方法外的变量为类变量，类的所有实例共享

    def __init__(self, name: str, age: int):
        """
        名为 __init__() 的特殊方法相当于类的构造方法。
        当创建的类实例时会调用 __init__()
        :param name:
        :param age:
        """
        self.name = name  # 绑定属性(public)
        self.age = age  # 绑定属性
        self.__foo = 0  # 双下划线开头的属性为私有属性（private），子类无法访问
        self._gender = "None"  # 单下划线开头为受保护的属性（protected）

    # 方法的第一个参数常常被命名为 self 表示类的示例对象本身，self 命名仅是一个约定
    def say(self, something: str):
        print(f"{self.name} say {something}")


class Student(Person):  # 继承语法： ClassName(BasicClassName)
    """Person类的派生类"""

    def __init__(self, name: str, age: int):
        super().__init__(name, age)  # 调用基类的构造函数
        self.courses = set()  # 课程列表，使用 set 不允许重复

    def add_course(self, course: str):
        self.courses.add(course)


def test1():
    tom = Student("Tom", 13)
    jack = Student("Jack", 14)

    tom.say("hello")
    jack.say("hello")
    tom.kind = "mankind"
    print(tom.kind, jack.kind)  # mankind human
    # print(tom.__foo)  # 无法防伪私有属性，报错：AttributeError: 'Student' object has no attribute '__foo'
    # 访问受保护的（protected）成员不会报错，但是不合理
    print(tom._gender)  # None


# test1()

"""
=== @property 装饰器 ===
虽然不建议将属性设置为私有的，但是如果直接将属性暴露给外界也是有问题的，比如我们没有办法检查赋给属性的值是否有效。
之前的建议是将属性命名以单下划线开头（protected），通过这种方式来暗示属性是受保护的，不建议外界直接访问，那么如果想访问属性可以通过属性的getter（访问器）和setter（修改器）方法进行对应的操作。
使用 @property 包装器来包装 getter 和 setter 方法，使得对属性的访问既安全又方便
"""


class Person(object):
    def __init__(self, name, age):
        self._name = name
        self._age = age

    # 访问器 - getter方法
    @property
    def name(self):
        return self._name

    # 访问器 - getter方法
    @property
    def age(self):
        return self._age

    # 修改器 - setter方法
    @age.setter
    def age(self, age):
        self._age = age

    def play(self):
        if self._age <= 16:
            print("%s正在玩飞行棋." % self._name)
        else:
            print("%s正在玩斗地主." % self._name)


def test2():
    person = Person("王大锤", 12)
    person.play()
    person.age = 22
    person.play()
    print(person.name)


# test2()


"""
=== __slots__ 魔法 ===
Python是一门动态语言：允许在程序运行时给对象绑定新的属性或方法，当然也可以对已经绑定的属性和方法进行解绑定。
可以通过在类中定义__slots__变量来限定：自定义类型的对象只能绑定某些属性。
需要注意：__slots__的限定只对当前类的对象生效，对子类并不起任何作用。
"""


class SlotsTest:
    # 限定SlotsTest对象只能绑定_name、_age、_gender 属性
    __slots__ = ["_name", "_age", "_gender"]

    def __init__(self, name, age):
        self._name = name
        self._age = age
        # self.age = age  # 定义了 __slots__，如果属性不在 __slots__ 列表中将报错


def test3():
    s = SlotsTest("张三", 18)
    s._gender = "男"  # 动态添加属性
    # s.age = 20 # 报错： AttributeError: 'SlotsTest' object has no attribute 'age'


# test3()

"""
=== 静态方法和类方法 ===
静态方法: 使用 @staticmethod 装饰器装饰的方法为静态方法，注意方法的第一个参数不是 self 或 cls
类方法：使用 @classmethod 装饰器装饰且方法的第一个参数约定名为 cls，它代表的是当前类相关的信息的对象（类本身也是一个对象，有的地方也称之为类的元数据对象），通过这个参数我们可以获取和类相关的信息并且可以创建出类的对象
"""


class Clock(object):
    """数字时钟"""

    class_des = "数字时钟"  # 类变量

    def __init__(self, hour=0, minute=0, second=0):
        self._hour = hour
        self._minute = minute
        self._second = second

    """
    使用 @staticmethod 装饰器
    由于静态方法没有 self 参数,所以它无法访问类的成员变量;
    静态方法也没有 cls 参数,所以它也无法访问类变量。
    静态方法跟定义它的类没有直接的关系,只是起到类似函数的作用。
    静态方法既可以通过对象名调用,也可以通过类名调用,二者没有任何区别
    """

    @staticmethod
    def print():
        print("static method")

    """使用 @classmethod 装饰器，方法的第一个参数命名为 cls，约定俗成改方法为一个类方法"""

    @classmethod
    def now(cls):
        ctime = localtime(time())
        print(cls.class_des)  # 注意：类方法是无法访问成员变量的,但是可以访问类变量
        # 调用 cls() 可以直接构造一个类的对象
        return cls(ctime.tm_hour, ctime.tm_min, ctime.tm_sec)

    # 成员方法，第一个参数为 self
    def run(self):
        """走字"""
        self._second += 1
        if self._second == 60:
            self._second = 0
            self._minute += 1
            if self._minute == 60:
                self._minute = 0
                self._hour += 1
                if self._hour == 24:
                    self._hour = 0

    def show(self):
        """显示时间"""
        return "%02d:%02d:%02d" % (self._hour, self._minute, self._second)


def test4():
    # 通过类方法创建对象并获取系统时间
    clock = Clock.now()
    counter = 0
    while counter < 5:
        print(clock.show())
        sleep(1)
        clock.run()
        counter += 1

    # 静态方法既可以通过对象名调用,也可以通过类名调用,二者没有任何区别
    Clock.print()
    clock.print()


# test4()

"""
=== 继承、重写和多态 ===
子类在继承了父类的方法后，可以对父类已有的方法给出新的实现版本，这个动作称之为方法重写（override）。
通过方法重写可以让父类的同一个行为在子类中拥有不同的实现版本，当我们调用这个经过子类重写的方法时，不同的子类对象会表现出不同的行为，这个就是多态（poly-morphism）

Python从语法层面并没有像Java或C#那样提供对抽象类的支持，但是可以通过 abc 模块的 ABCMeta 元类和 @abstractmethod 包装器来达到抽象类的效果
Python 的 abc 模块，是 Python 对 Abstract Basic Class （抽象基类）的支持模块。
abc 模块定义了一个特殊的 metaclass：ABCMeta 和 装饰器： @abstractmethod 和 @abstractproperty 
 abc.ABCMeta : 是一个 metaclass，用于在Python程序中创建抽象基类（元类：用于创建类的类）
 @abstractmethod : 用于装饰抽象函数
 @abstractproperty : 用于装饰抽象属性，已过时，使用 @property + @abstractmethod 装饰器
 
=== 类之间的关系 ===
简单的说，类和类之间的关系有三种：is-a、has-a和use-a关系:
 is-a  关系:也叫【继承】或【泛化】，比如学生和人的关系、手机和电子产品的关系都属于继承关系。
 has-a 关系:通常称之为【关联】，比如部门和员工的关系，汽车和引擎的关系都属于关联关系；
           关联关系如果是整体和部分的关联，那么我们称之为【聚合关系】；
           如果整体进一步负责了部分的生命周期（整体和部分是不可分割的，同时同在也同时消亡），那么这种就是最强的关联关系 称之为【合成关系】。
 use-a 关系:通常称之为【依赖】，比如司机有一个驾驶的行为（方法），其中（的参数）使用到了汽车，那么司机和汽车的关系就是依赖关系。
"""

from abc import ABCMeta, abstractmethod


# Pet类为一个抽象类：不能够创建对象的类，这种类的存在就是专门为了让其他类去继承它
# abc.ABCMeta 是实现抽象类的一个基础类（元类）
# 这里设置 metaclass=abc.ABCMeta 的含义是告诉解释器：使用 ABCMeta 这个元类来创建 Pet 类
class Pet(object, metaclass=ABCMeta):
    """宠物"""

    def __init__(self, nickname):
        self._nickname = nickname

    # 使用 @property + @abstractmethod 装饰器定义 抽象属性
    # @abstractproperty 装饰器已过时
    @property
    @abstractmethod
    def species(self):
        """物种"""
        pass

    # @abstractmethod 定义抽象方法，无需实现功能
    @abstractmethod
    def make_voice(self):
        """发出声音"""
        pass


class Dog(Pet):
    """狗"""

    @property
    def species(self):
        return "Dog"

    def make_voice(self):
        print("%s: 汪汪汪..." % self._nickname)


class Cat(Pet):
    """猫"""

    @property
    def species(self):
        return "Cat"

    def make_voice(self):
        print("%s: 喵...喵..." % self._nickname)


"""
=== 什么是 metaclass ? ===
参考：https://www.cnblogs.com/panwenbin-logs/p/13524715.html

元类(metaclass) ：就是创建类的类
metaclass是“类的类”，秉承Python“一切皆对象”的理念，Python中的【类】也是一类对象，metaclass的实例就是类（class），自己写metaclass时需要让其继承自type对象。
也就是说metaclass的实例化结果是类，而class实例化的结果是instance
可以这么理解的： metaclass是创建类的模板，所有的类都是通过他来create的(调用__new__)
metaclass主要用处：
 + 你可以自由的、动态的修改/增加/删除 类的或者实例中的方法或者属性
 + 批量的对某些方法使用decorator，而不需要每次都在方法的上面加入 @decorator_func
 + 当引入第三方库的时候，如果该库某些类需要patch的时候可以用metaclass
 + 可以用于序列化(参见yaml这个库的实现，我没怎么仔细看）
 + 提供接口注册，接口格式检查等
 + 自动委托(auto delegate)
 
所有的对象都是实例化或者说调用类而得到的（调用类的过程称为类的实例化),python内置了一个方法可以看到类对象的类，就是 type
"""

dog = Dog("Tom")
print(type(dog))  # 查看dog对象的类型：<class '__main__.Dog'>
print(type(Dog))  # 查看Dog类的类型： <class 'abc.ABCMeta'>,证明是调用了abc.ABCMeta这个元类而产生的Dog
p = Person("张三", 18)
print(type(p))  # 查看p对象的类型：<class '__main__.Person'>
print(type(Person))  # 查看Person类的类型： <class 'type'>,证明是调用了type这个元类而产生的Person，即默认的元类为type

"""
由上可以推导出类的对象的实例化(Instancing)过程：

|-----------------|              |--------------|                |----------|
| metaclass(type) |--type(...)-->| class Person |--Person(...)-->| Object p |
|-----------------|  Instancing  |--------------|   Instancing   |----------|

=== class关键字创建类的流程分析 ===
用 class 关键字定义的类本身也是一个对象，负责产生该对象的类称之为 元类 metaclass（元类可以简称为类的类），内置的元类为 type
在使用 class 关键字创建类时，必然调用了元类 Person = type(...)，那调用 type 时传入的参数是什么呢？必然是类的关键组成部分，一个类有三大组成部分，分别是：
 1. 类名 class_name = 'StanfordTeacher'
 2. 基类们 class_bases = (object,) 注意是一个元祖，默认为 (object,)
 3. 类的名称空间 class_dic，即：包含属性的字典（名称和值），类的名称空间是执行类体代码而得到的
"""


def create_person_class():
    """class关键字底层的做了哪些事?"""
    # 1、先拿到一个类名
    class_name = "Person"

    # 2、然后拿到类的父类
    class_bases = (object,)

    # 3、再运行类体代码，将产生的名字放到名称空间中
    class_dic = {}
    # 类的成员定义，文本格式。注意缩进要符合Python语法
    class_body = """
school = 'oldboy'

def __init__(self, name, age):
    self.name = name
    self.age = age

def say(self):
    print('%s says welcome to the oldboy to learn Python' % self.name)
    """
    exec(class_body, {}, class_dic)
    # print(class_dic)

    # 4、调用元类（传入类的三大要素：类名、基类、类的名称空间）得到一个元类的对象，Person,Person就是我们用class自定义的那个类
    Person = type(class_name, class_bases, class_dic)  # <class '__main__.Person'>
    print("使用 type(class_name, class_bases, class_dic) 创建的类：", Person)


create_person_class()

"""
=== 自定义元类控制类的创建 ===
一个类没有声明自己的元类，默认他的元类就是type，除了使用内置元类type，我们也可以通过继承type来自定义元类，然后使用metaclass关键字参数为一个类指定元类
"""
import re


# 自定义元类，只有继承了type类的类才是自定义的元类
class MyMetaClass(type):
    """自定义元类，自定义元类来控制StanfordTeacher类的产生"""

    def __init__(self, class_name, class_bases, class_dic):
        """
        初始化元类
        :param class_name: 该元类创建的类的名字
        :param class_bases: 该元类创建的类的基类（元组类型）
        :param class_dic: 该元类创建的类的命名空间（属性和方法描述）
        """
        print(self)  # 类<class '__main__.StanfordTeacher'>
        print(class_name)
        print(class_bases)
        print(class_dic)

        if not re.match("[A-Z]", class_name):
            raise Exception("类名必须用驼峰体")

        if len(class_bases) == 0:
            raise Exception("至少继承一个父类")

        # print("文档注释：",class_dic.get('__doc__'))
        doc = class_dic.get("__doc__")

        if not (doc and len(doc.strip()) > 0):
            raise Exception("必须要有文件注释，并且注释内容不为空")

    # res = StanfordTeacher('egon',18)
    def __call__(self, *args, **kwargs):
        # 1、先创建一个老师的空对象
        obj = object.__new__(self)
        # 2、调用老师类内的__init__函数，然后将老师的空对象连同括号内的参数的参数一同传给__init__
        self.__init__(obj, *args, **kwargs)
        obj.__dict__ = {
            "_%s__%s" % (self.__name__, k): v for k, v in obj.__dict__.items()
        }

        # 3、将初始化好的老师对象赋值给变量名res
        return obj


# StanfordTeacher = MyMetaClass("StanfordTeacher",(object,),{...})
class StanfordTeacher(object, metaclass=MyMetaClass):
    """声明类，指定创建类的元类为自定义元类 MyMetaClass"""

    school = "oldboy"

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def say(self):
        print("%s says welcome to the oldboy to learn Python" % self.name)


"""
=== 扩展知识：__call__，__new__，__init__ 三个特殊方法 ===
__new__(cls, *more) 方法：对象的创建，是一个静态方法，第一个参数是 cls。(如果没定义，则会调用父类的 __new__ 方法，以此类推，直至找到顶层的 object.__new__(cls) 去创建一个对象)
__init__(self) 方法：对象的初始化， 是一个实例方法，第一个参数是self。
__call__(self) 方法：该类对象调用__call__，注意不是类!。

在Python当中，__call__,__new__,__init__三者之间的关系:
在类实例化的过程当中，如 Person(...) 就寻找产生这个对象的类的__call__方法，只要是__call__方法，一定会做三件事情：
 第一：调用__new__方法，构造新的对象，相当于Java当中的构造函数.（对象自己的__new__，如果没定义，默认会调用 object.__new__ 去创建一个对象）
 第二：调用__init__方法,去初始化这个对象（对象自己的__init__）
 第三：返回这个对象.
 
类在实例化对象的时候函数的调用顺序依次是:
__call__ ==> __new__ ==> __init__
"""


class Foo(object):
    """Foo"""

    def __init__(self, name):
        self.name = name
        print("Foo.__init__")

    def __new__(cls, *args, **kwargs):
        """
        类函数，用于创建 Foo 类的实例对象。
        注意:第一个参数为 cls 表示当前类型
        :param args:
        :param kwargs:
        """
        print("Foo.__new__")
        # 调用父类的__new__()返回对象
        # 注意：这里先创建一个空对象，然后在调用 __init__ 方法对该对象进行初始化，最后返回
        return super().__new__(cls)

    def __call__(self, *args, **kwargs):
        """
        __call__的作用就是声明一个对象是可调用的。
        即实现__call__方法之后，用callable调用这个类的对象时，结果为true。
        实例对象可以像调用函数那样直接调用，如 f() -> f 为 Foo 的实例对象
        :param args:
        :param kwargs:
        :return:
        """
        print("Foo.__call__")

    def __str__(self):
        return f"Foo {{name:{self.name}}}"


f = Foo("foo")
# Foo.__new__
# Foo.__init__
print(f)
# Foo {name:foo}
print("Foo instance f is callable ", callable(f))
# Foo instance f is callable  True
f()
# Foo.__call__
